// Copyright 2021 by 马万里. All rights reserved.
// 开发团队 ：   鸡中之霸
// 开发人员 ：   马万里
// 开发时间 ：   2021/2/25 15:57
// 文件名称 ：   mpupload.go
// 工程名称 ：   netpan
// 开发工具 ：   GoLand
//

package handler

import (
	"fmt"
	"github.com/garyburd/redigo/redis"
	rPool "johnsonsmile.cn/netpan/cache/redis"
	myDB "johnsonsmile.cn/netpan/db"
	util "johnsonsmile.cn/netpan/utils"
	"math"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

// MiltipartUploadInfo: 分块上传的初始化信息
type MiltipartUploadInfo struct {
	FileHash   string
	FileSize   int
	UploadID   string
	ChunkSize  int
	ChunkCount int
	// 已经上传完成的分块索引列表
	ChunkExists []int
}

const (
	// ChunkDir: 上传的分块所在目录
	ChunkDir = "/data/chunks/"
	// MergeDir: 合并后的文件所在的目录
	MergeDir = "/data/merge/"
	// ChunkKeyPrefix: 分块信息对应的redis键前缀
	ChunkKeyPrefix = "MP_"
	// HashUpIDKeyPrefix: 文件hash映射uploadid对应的redis键前缀
	HashUpIDKeyPrefix = "HASH_UPID_"
)

func init() {
	//if err := os.MkdirAll(ChunkDir, 0744); err != nil {
	//	fmt.Println("无法指定目录用于存储分块文件:" + ChunkDir)
	//	os.Exit(1)
	//}
	//if err := os.MkdirAll(MergeDir, 0744); err != nil {
	//	fmt.Println("无法指定目录用于存储合成后的文件:" + MergeDir)
	//	os.Exit(1)
	//}
}

// 初始化分块上传
func InitialMultipartUploadHandler(w http.ResponseWriter, r *http.Request) {
	// 1. 解析用户参数
	r.ParseForm()
	username := r.Form.Get("username")
	filehash := r.Form.Get("filehash")
	filesize, err := strconv.Atoi(r.Form.Get("filesize"))
	if err != nil {
		w.Write(util.NewRespMsg(-1, "params invalid", nil).JSONBytes())
		return
	}
	// 2. 获得redis连接
	rConn := rPool.RedisPool().Get()
	defer rConn.Close()

	var uploadID string
	// 3. 通过文件hash判断是否断点续传,并获取uploadID
	keyExists, _ := redis.Bool(rConn.Do("EXISTS", HashUpIDKeyPrefix+filehash))
	if keyExists {
		uploadID, err = redis.String(rConn.Do("GET", HashUpIDKeyPrefix+filehash))
		if err != nil {
			w.Write(util.NewRespMsg(-1, "upload part failed", err.Error()).JSONBytes())
			return
		}
	}

	// 4.1 首次上传则创建uploadID
	// 4.2 首次断点续传,则根据uploadID获取已上传的文件的分块列表
	chunksExist := []int{}
	if uploadID == "" {
		uploadID = username + fmt.Sprintf("%x", time.Now().UnixNano())
	} else {
		chunks, err := redis.Values(rConn.Do("HGETALL", ChunkKeyPrefix+filehash))
		if err != nil {
			w.Write(util.NewRespMsg(-1, "upload part failed", err.Error()).JSONBytes())
			return
		}
		for i := 0; i < len(chunks); i += 2 {
			k := string(chunks[i].([]byte))
			v := string(chunks[i+1].([]byte))
			if strings.HasPrefix(k, "chkidx_") && v == "1" {
				chunkidx, _ := strconv.Atoi(k[7:])
				chunksExist = append(chunksExist, chunkidx)
			}
		}
	}

	// 5. 生成分块上传的初始化信息
	upInfo := MiltipartUploadInfo{
		FileHash:    filehash,
		FileSize:    filesize,
		UploadID:    uploadID,
		ChunkSize:   5 * 1024 * 1024, // 5MB
		ChunkCount:  int(math.Ceil(float64(filesize) / 5 * 1024 * 1024)),
		ChunkExists: chunksExist,
	}
	// 6. 将初始化信息写入到redis缓存中,仅在首次上传的时候调用
	if len(chunksExist) <= 0 {
		hkey := ChunkKeyPrefix + upInfo.UploadID
		rConn.Do("HSET", hkey, "chunkcount", upInfo.ChunkCount)
		rConn.Do("HSET", hkey, "filehash", upInfo.FileHash)
		rConn.Do("HSET", hkey, "filesize", upInfo.FileSize)
		rConn.Do("EXPIE", hkey, 43200)                                            // 12小时
		rConn.Do("SET", HashUpIDKeyPrefix+filehash, upInfo.UploadID, "EX", 43200) // 12小时
	}

	// 7. 将响应初始化数据返回到客户端
	w.Write(util.NewRespMsg(0, "OK", upInfo).JSONBytes())

}

// UploadPartHandler: 上传文件分块
func UploadPartHandler(w http.ResponseWriter, r *http.Request) {
	// 1. 解析用户请求参数
	r.ParseForm()
	//username := r.Form.Get("username")
	uploadId := r.Form.Get("uploadid")
	chunkIndex := r.Form.Get("index")
	// 2. 获得redis连接池中的连接
	rConn := rPool.RedisPool().Get()
	defer rConn.Close()
	// 3. 获得文件句柄,用于存储分块内容
	os.MkdirAll("/data/"+uploadId, 0744)
	fd, err := os.Create("/data/" + uploadId + "/" + chunkIndex)
	if err != nil {
		w.Write(util.NewRespMsg(-1, "Upload part failed", nil).JSONBytes())
		return
	}
	defer fd.Close()
	buf := make([]byte, 1024*1024)
	for {
		n, err := r.Body.Read(buf)
		fd.Write(buf[:n])
		if err != nil {
			break
		}
	}
	// 4. 更新redis缓存状态
	rConn.Do("HSET", "MP_"+uploadId, "chkidx_"+chunkIndex, 1)
	// 5. 返回处理结果到客户端
	w.Write(util.NewRespMsg(0, "OK", nil).JSONBytes())
}

// CompleteUploadHandler: 通知上传合并
func CompleteUploadHandler(w http.ResponseWriter, r *http.Request) {
	// 1. 获取参数
	r.ParseForm()
	upid := r.Form.Get("uploadid")
	username := r.Form.Get("username")
	filehash := r.Form.Get("filehash")
	filesize, _ := strconv.Atoi(r.Form.Get("filesize"))
	filename := r.Form.Get("filename")

	// 2. 获取redis连接池连接
	rConn := rPool.RedisPool().Get()
	defer rConn.Close()

	// 3. 通过uploadid,查询是否所有分块上传完成
	data, err := redis.Values(rConn.Do("HGETALL", "MP_"+upid))
	if err != nil {
		w.Write(util.NewRespMsg(-1, "complete upload failed", nil).JSONBytes())
		return
	}
	totalCount := 0
	chunkCount := 0
	for i := 0; i < len(data); i += 2 {
		k := string(data[i].([]byte))
		v := string(data[i+1].([]byte))
		if k == "chuckcount" {
			totalCount, _ = strconv.Atoi(v)
		} else if strings.HasPrefix(k, "chkidx_") && v == "1" {
			chunkCount += 1
		}
	}
	if totalCount != chunkCount {
		w.Write(util.NewRespMsg(-2, "invalid request", nil).JSONBytes())
		return
	}

	// 5. TODO: 分块合并操作

	// 6. 更新唯一文件表,以及用户文件表
	suc := myDB.OnFileUploadFinished(filehash, filename, int64(filesize), "")
	finished := myDB.OnUserFileUploadFinished(username, filehash, filename, int64(filesize))

	// 7. 响应处理结果
	if suc && finished {
		w.Write(util.NewRespMsg(0, "OK", nil).JSONBytes())
		return
	}
	w.Write(util.NewRespMsg(-1, "CompleteUploadHandler failed", nil).JSONBytes())
	return
}

// CancelUploadHandler: 取消分块上传接口
func CancelUploadHandler(w http.ResponseWriter, r *http.Request) {

	// 1. 解析用户参数
	filehash := r.Form.Get("filehash")
	// 2. 获取redis连接
	rConn := rPool.RedisPool().Get()
	defer rConn.Close()

	// 3. 检查uploadID
	uploadID, err := redis.String(rConn.Do("GET", HashUpIDKeyPrefix+filehash))
	if err != nil || uploadID == "" {
		w.Write(util.NewRespMsg(-1, "Cancel upload part failed", nil).JSONBytes())
		return
	}
	_, delErr := rConn.Do("DEL", HashUpIDKeyPrefix+filehash)
	_, delUploadInfoErr := rConn.Do("DEL", ChunkKeyPrefix+filehash)
	if delUploadInfoErr != nil || delErr != nil {
		w.Write(util.NewRespMsg(-2, "Cancel upload part failed", nil).JSONBytes())
		return
	}

	// 4. 删除已经上传的分块文件
	delChkRes := util.RemovePathByShell(ChunkDir + uploadID)
	if !delChkRes {
		fmt.Printf("Failed to delete chunks as upload canceled, uploadID: %s\n", uploadID)
	}
	// 5. 响应客户端
	w.Write(util.NewRespMsg(0, "OK", nil).JSONBytes())
}
